// Copyright (C) 2010, Kyle Lemons <kyle@kylelemons.net>.  All rights reserved.

package log4go

import (
	"fmt"
	"io/ioutil"
	"os"
	"path"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"time"
)

// This log writer sends output to a file
type FileLogWriter struct {
	rec chan *LogRecord
	rot chan bool

	// The opened file
	filename string
	file     *os.File

	// The logging format
	format string

	// File header/trailer
	header, trailer string

	// Rotate at linecount
	maxlines          int
	maxlines_curlines int

	// Rotate at size
	maxsize         int
	maxsize_cursize int

	// Rotate daily
	daily          bool
	daily_opendate int

	// Keep old logfiles (.001, .002, etc)
	rotate bool

	maxrotate int

	//当前日志文件的下一个num号和最小num号, 取值范围>=1
	nextnum int
	minnum  int
}

// This is the FileLogWriter's output method
func (w *FileLogWriter) LogWrite(rec *LogRecord) {
	w.rec <- rec
}

func (w *FileLogWriter) Close() {
	close(w.rec)
	w.file.Sync()
}

// NewFileLogWriter creates a new LogWriter which writes to the given file and
// has rotation enabled if rotate is true.
//
// If rotate is true, any time a new log file is opened, the old one is renamed
// with a .### extension to preserve it.  The various Set* methods can be used
// to configure log rotation based on lines, size, and daily.
//
// The standard log-line format is:
//   [%D %T] [%L] (%S) %M
func NewFileLogWriter(fname string, rotate bool, maxrotate int) *FileLogWriter {
	w := &FileLogWriter{
		rec:       make(chan *LogRecord, LogBufferLength),
		rot:       make(chan bool),
		filename:  fname,
		format:    "[%D %T] [%L] (%S) %M",
		rotate:    rotate,
		maxrotate: maxrotate,
	}

	if err := w.initNum(time.Now()); err != nil {
		fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.filename, err)
		return nil
	}

	// open the file for the first time
	if err := w.intRotate(false); err != nil {
		fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.filename, err)
		return nil
	}

	go func() {
		defer func() {
			if w.file != nil {
				fmt.Fprint(w.file, FormatLogRecord(w.trailer, &LogRecord{Created: time.Now()}))
				w.file.Close()
			}
		}()

		for {
			select {
			case <-w.rot:
				if err := w.intRotate(true); err != nil {
					fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.filename, err)
					return
				}
			case rec, ok := <-w.rec:
				if !ok {
					return
				}
				now := time.Now()
				if (w.maxlines > 0 && w.maxlines_curlines >= w.maxlines) ||
					(w.maxsize > 0 && w.maxsize_cursize >= w.maxsize) ||
					(w.daily && now.Day() != w.daily_opendate) {
					if err := w.intRotate(true); err != nil {
						fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.filename, err)
						return
					}
				}

				// Perform the write
				n, err := fmt.Fprint(w.file, FormatLogRecord(w.format, rec))
				if err != nil {
					fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.filename, err)
					return
				}

				// Update the counts
				w.maxlines_curlines++
				w.maxsize_cursize += n
			}
		}
	}()

	return w
}

type FileSlice []string

func (p FileSlice) Len() int { return len(p) }

func (p FileSlice) Less(i, j int) bool {
	f1, err1 := os.Lstat(p[i])
	if err1 != nil {
		return false
	}
	f2, err2 := os.Lstat(p[j])
	if err2 != nil {
		return true
	}
	return f1.ModTime().Before(f2.ModTime())
}

func (p FileSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }

// Request that the logs rotate
func (w *FileLogWriter) Rotate() {
	w.rot <- true
}

func (w *FileLogWriter) initNum(today time.Time) error {
	nameIndex := strings.LastIndex(w.filename, "/")
	todayFormat := w.filename[nameIndex+1:] + fmt.Sprintf(".%s.", today.Format("2006-01-02"))
	dirPth := path.Dir(w.filename)
	dir, err := ioutil.ReadDir(dirPth)
	if err != nil {
		return err
	}

	w.nextnum = 0
	w.minnum = 0
	for _, fi := range dir {
		if fi.IsDir() { // 忽略目录
			continue
		}
		index := strings.Index(fi.Name(), todayFormat)
		if index < 0 {
			continue
		}
		index += len(todayFormat)
		tmp, err := strconv.Atoi(fi.Name()[index:])
		if err != nil {
			continue
		}
		if tmp > w.nextnum {
			w.nextnum = tmp
		}
		if w.minnum == 0 || tmp < w.minnum {
			w.minnum = tmp
		}
	}
	w.nextnum++
	if w.minnum == 0 {
		w.minnum = 1
	}
	return nil
}

func (w *FileLogWriter) rotateFile(t time.Time) string {
	//maxrotate <= 0这种情况不会清除任何文件
	timeformat := t.Format("2006-01-02")
	if (w.maxrotate > 0) && (w.nextnum > w.maxrotate) && (w.nextnum-w.minnum >= w.maxrotate) {
		minfname := w.filename + fmt.Sprintf(".%s.%03d", timeformat, w.minnum)
		os.Remove(minfname)
		w.minnum++
	}
	fname := w.filename + fmt.Sprintf(".%s.%03d", timeformat, w.nextnum)
	w.nextnum++
	return fname
}

func (w *FileLogWriter) DeleteOldLogs(today time.Time, n int) int {
	deleted := 0
	dir := path.Dir(w.filename)
	todayFiles := make([]string, 0, w.maxrotate)
	filepath.Walk(dir, func(filepath string, f os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if f.IsDir() {
			return nil
		}
		//删除今天之前的文件（）
		if f.ModTime().Day() != today.Day() {
			//			if os.Remove(filepath) == nil {
			//				deleted++
			//			}
		} else {
			todayFiles = append(todayFiles, filepath)
		}
		return nil
	})

	//按时间排序
	sort.Sort(FileSlice(todayFiles))

	//删除今天前20%的文件
	if n <= 0 {
		n = 1
	}
	if n > len(todayFiles) {
		n = len(todayFiles)
	}
	for i := 0; i < n; i++ {
		newpath := todayFiles[i] + ".delete"
		for i := 0; i < 2; i++ {
			if err := os.Rename(todayFiles[i], newpath); err == nil {
				break
			}
			os.Remove(newpath)
		}
		err := os.Remove(newpath)
		if err == nil {
			deleted++
		}
	}
	return deleted
}

// If this is called in a threaded context, it MUST be synchronized
// huangcheng edit(2014-11): add rotateflag to disable rotate (in initial period)
func (w *FileLogWriter) intRotate(rotateflag bool) error {
	// Close any log file that may be open
	if w.file != nil {
		fmt.Fprint(w.file, FormatLogRecord(w.trailer, &LogRecord{Created: time.Now()}))
		w.file.Close()
	}

	// If we are keeping log files, move it to the next available number
	if rotateflag && w.rotate {
		_, err := os.Lstat(w.filename)
		if err == nil { // file exists
			// Find the next available number
			//			num := 1
			fname := ""
			if w.daily && time.Now().Day() != w.daily_opendate {
				ydaytime := time.Now().AddDate(0, 0, -1)
				fname = w.rotateFile(ydaytime)
				w.nextnum = 1
				w.minnum = 1
				//				yesterday := ydaytime.Format("2006-01-02")
				//				for loop := 0; loop < 2; loop++ {
				//					for ; err == nil && num <= w.maxrotate; num++ {
				//						fname = w.filename + fmt.Sprintf(".%s.%03d", yesterday, num)
				//						_, err = os.Lstat(fname)
				//					}
				//					if err != nil {
				//						break
				//					}
				//					if loop == 0 {
				//						w.DeleteOldLogs(ydaytime, 1)
				//						num = 1
				//					}
				//				}

			} else {
				fname = w.rotateFile(time.Now())
				//				for loop := 0; loop < 2; loop++ {
				//					for ; err == nil && num <= w.maxrotate; num++ {
				//						fname = w.filename + fmt.Sprintf(".%s.%03d", time.Now().Format("2006-01-02"), num)
				//						_, err = os.Lstat(fname)
				//					}
				//					if err != nil {
				//						break
				//					}
				//					if loop == 0 {
				//						//删除20%的日志
				//						w.DeleteOldLogs(time.Now(), int(float32(w.maxrotate)*0.2))
				//						num = 1
				//					}
				//				}
			}
			// return error if the last file checked still existed
			//			if err == nil {
			//				return fmt.Errorf("Rotate: Cannot find free log number to rename %s\n", w.filename)
			//			}
			w.file.Close()
			// Rename the file to its newfound home
			err = os.Rename(w.filename, fname)
			if err != nil {
				return fmt.Errorf("Rotate: %s\n", err)
			}
		}
	}

	// Open the log file
	perm := os.FileMode(0666)
	fd, err := os.OpenFile(w.filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, perm)
	if err != nil {
		return err
	}
	os.Chmod(w.filename, perm)
	w.file = fd

	now := time.Now()
	fmt.Fprint(w.file, FormatLogRecord(w.header, &LogRecord{Created: now}))

	// Set the daily open date to the current date
	w.daily_opendate = now.Day()

	// initialize rotation values
	w.maxlines_curlines = 0
	w.maxsize_cursize = 0

	return nil
}

// Set the logging format (chainable).  Must be called before the first log
// message is written.
func (w *FileLogWriter) SetFormat(format string) *FileLogWriter {
	w.format = format
	return w
}

// Set the logfile header and footer (chainable).  Must be called before the first log
// message is written.  These are formatted similar to the FormatLogRecord (e.g.
// you can use %D and %T in your header/footer for date and time).
func (w *FileLogWriter) SetHeadFoot(head, foot string) *FileLogWriter {
	w.header, w.trailer = head, foot
	if w.maxlines_curlines == 0 {
		fmt.Fprint(w.file, FormatLogRecord(w.header, &LogRecord{Created: time.Now()}))
	}
	return w
}

// Set rotate at linecount (chainable). Must be called before the first log
// message is written.
func (w *FileLogWriter) SetRotateLines(maxlines int) *FileLogWriter {
	//fmt.Fprintf(os.Stderr, "FileLogWriter.SetRotateLines: %v\n", maxlines)
	w.maxlines = maxlines
	return w
}

// Set rotate at size (chainable). Must be called before the first log message
// is written.
func (w *FileLogWriter) SetRotateSize(maxsize int) *FileLogWriter {
	//fmt.Fprintf(os.Stderr, "FileLogWriter.SetRotateSize: %v\n", maxsize)
	w.maxsize = maxsize
	return w
}

// Set rotate daily (chainable). Must be called before the first log message is
// written.
func (w *FileLogWriter) SetRotateDaily(daily bool) *FileLogWriter {
	//fmt.Fprintf(os.Stderr, "FileLogWriter.SetRotateDaily: %v\n", daily)
	w.daily = daily
	return w
}

// SetRotate changes whether or not the old logs are kept. (chainable) Must be
// called before the first log message is written.  If rotate is false, the
// files are overwritten; otherwise, they are rotated to another file before the
// new log is opened.
func (w *FileLogWriter) SetRotate(rotate bool) *FileLogWriter {
	//fmt.Fprintf(os.Stderr, "FileLogWriter.SetRotate: %v\n", rotate)
	w.rotate = rotate
	return w
}

// NewXMLLogWriter is a utility method for creating a FileLogWriter set up to
// output XML record log messages instead of line-based ones.
func NewXMLLogWriter(fname string, rotate bool, maxrotate int) *FileLogWriter {
	return NewFileLogWriter(fname, rotate, maxrotate).SetFormat(
		`	<record level="%L">
		<timestamp>%D %T</timestamp>
		<source>%S</source>
		<message>%M</message>
	</record>`).SetHeadFoot("<log created=\"%D %T\">", "</log>")
}
